package com.jfdesgagne.traklib.tracker {
	import com.jfdesgagne.traklib.events.TrakMultiEvent;
	import com.jfdesgagne.traklib.geom.TrakColourMatrix;
	import com.jfdesgagne.traklib.geom.TrakRectangle;

	import flash.display.BitmapData;
	import flash.display.DisplayObjectContainer;
	import flash.events.EventDispatcher;
	import flash.filters.BlurFilter;
	import flash.filters.ColorMatrixFilter;
	import flash.filters.ConvolutionFilter;
	import flash.geom.Matrix;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.media.Video;

	/**
	 * @author jdesgagne
	 */
	public class AbstractTrak extends EventDispatcher {
		protected var __oldFrame:BitmapData;		protected var __nowFrame:BitmapData;
		protected var __difFrame:BitmapData;
				protected var __input:Video;	
		protected var __refStage:DisplayObjectContainer;
		protected var __blurFilter:BlurFilter;
		protected var __colourMatrix:TrakColourMatrix;
		protected var __colourMatrixFilter:ColorMatrixFilter;
		protected var __box:Rectangle;
		protected var __act:Boolean;
		protected var __matrix:Matrix;
		protected var __min:Number;
		protected var __max:Number;
		protected var __boxes:Array;		protected var __multiTracking:Boolean;
		
		protected var __recs_watching:Array = [];		protected var __min_size_new_rect_rate:Number = 3500;		protected var __max_size_new_rect_rate:Number = 15000;
		protected var __idCounter:int = 0;		protected var __rect_life:int = 5;
		protected var __noiseReduction:ConvolutionFilter;
		protected var __noize_min_pix:int = 7;
		protected var __colorDiffs:Array = [];
		protected var __threshold:uint = 0xff101010;
		protected var __gray:TrakColourMatrix;
		protected var __grayCm:ColorMatrixFilter;
		protected var __controlMaxUnionSize:Boolean = false;

		public function AbstractTrak(_input:Video, _refStage:DisplayObjectContainer) {
			this.input = _input; //must use the setter
			this.__colourMatrix = new TrakColourMatrix();
			this.__blurFilter = new BlurFilter();	
			this.__refStage = _refStage;
			
			this.__noiseReduction = new ConvolutionFilter();
			this.__noiseReduction.matrixX = 3;
			this.__noiseReduction.matrixY = 3;
			this.__noiseReduction.matrix = [
				1, 1, 1,
				1, 0, 1,
				1, 1, 1
			];
			this.__noiseReduction.bias = -0x100 * this.__noize_min_pix;
			
			this.__gray = new TrakColourMatrix();
			this.__gray.saturation = 0;
			this.__grayCm = new ColorMatrixFilter(this.__gray.getMatrix());
		}
		
		public function track():void {
			this.__box = null;
			this.__boxes = null;
			
			if(this.__multiTracking) {
				this.multipleDetection();
			} else {
				this.singleDetection();
			}
		}
		
		protected function get changed():Boolean {
			this.__difFrame.draw(this.__input, this.__matrix);
			var cnt:Number= 0;
			var changed:Boolean = false;
			for(var h:int=1; h<4; h++){
				for (var v:int=1; v<3; v++){
					var colorNow:Number = this.__difFrame.getPixel(this.__nowFrame.width/4*h, this.__nowFrame.height/3*v);
					if(colorNow != this.__colorDiffs[cnt]) changed = true;
					this.__colorDiffs[cnt] = colorNow;
					cnt ++;
				}
			}
			return changed;
		}
		

		public function applyFilters():void {
			if(this.__colourMatrixFilter) this.__nowFrame.applyFilter(this.__nowFrame, this.__nowFrame.rect, new Point(), this.__colourMatrixFilter);
			if(this.__blurFilter) this.__nowFrame.applyFilter(this.__nowFrame, this.__nowFrame.rect, new Point(), this.__blurFilter);		}
		
		protected function singleDetection():void {
			var area:Rectangle = this.__nowFrame.getColorBoundsRect(0xFFFFFFFF, 0xFFFFFFFF, true);
			this.__act = ( area.width > ( this.__input.width / 100) * this.__min || area.height > (this.__input.height / 100) * this.__min );
			
			if ( this.__act ) {
				this.__box = area;
				this.__box.x + (this.__box.width / 2);
				this.__box.y + (this.__box.width / 2);
			}
		}
		
		protected function multipleDetection():void {
			this.__boxes = [];
			var recs:Array = [];
			var i:int;
			var _rect_inflation:Number = 5;
			var recs_candidates:Array = [];
			var recs_disappeared:Array = [];
			var rec_watch:TrakRectangle;
			var rec_ref:TrakRectangle;
			var tgt_rect:TrakRectangle;
			var union:TrakRectangle;
			
			
			for(var element:String in this.__recs_watching){
				this.__recs_watching[element].isects = [];
			}
			
			
			while(true) {
				var rectDetect:Rectangle = this.__nowFrame.getColorBoundsRect(0xFFFFFF, 0xFFFFFF);
				if(rectDetect.isEmpty()) break;
				var wbr:TrakRectangle = new TrakRectangle(rectDetect);
				
				var x:int = wbr.x;
				for (var y:int = wbr.y + wbr.height - 1; y >= wbr.y; --y) {
					if (this.__nowFrame.getPixel(x, y) == 0xFFFFFF) {
						this.__nowFrame.floodFill(x, y, 0xFF00FF);
						var trackingRectangle:TrakRectangle = new TrakRectangle(this.__nowFrame.getColorBoundsRect(0xFFFFFF, 0xFF00FF));
						trackingRectangle.inflate(_rect_inflation,_rect_inflation);
						this.__nowFrame.fillRect(trackingRectangle, 0xFF0000);
						recs.push(trackingRectangle);
						break;
					}
				}
			}
			//trace('*****************************');
			
			
			for(var idx:int=0; idx<recs.length; idx++){
				var idx_rec:TrakRectangle = recs[idx];
				for(i=idx+1; i<recs.length; i++){
					var tgt_rec:TrakRectangle = recs[i];
					if( idx_rec!=tgt_rec && idx_rec.intersects(tgt_rec) ){
						union = idx_rec.union_rect(tgt_rec);
						//trace('union area:'+(union.width*union.height));
						if(union.width*union.height<this.__max_size_new_rect_rate) {
							recs.push(union);
							recs.splice(Number(i), 1);
							recs.splice(Number(idx), 1);
							idx --;
							break;
						} else {
							//trace('too big to be an union');
						}
					}
				}
			}
			//trace(recs);
			/*
			for(var o:String in this.__recs_watching){
				this.__recs_watching[o].isects = [];
			}
			*/
			
			for(i=0; i<recs.length; i++){
				rec_ref = recs[i];
				if(rec_ref.width*rec_ref.height >= this.__nowFrame.height*this.__nowFrame.width){
					recs.splice(Number(i), 1);
					i--;
				}else{
					rec_ref.isects = [];					
					
					//var oldWatch:Array = rec_watch.isects;
					
					for(var o:String in this.__recs_watching){
						rec_watch = this.__recs_watching[o];
						//rec_watch.isects = [];
						if(rec_watch.intersects(rec_ref)){
							rec_watch.isects.push(rec_ref);
							rec_ref.isects.push(rec_watch);
						}
					}
				}
			}
			
			//trace(recs);
			
			for(i=0; i<recs.length; i++){
				rec_ref = recs[i];
				var isectsNum:int = rec_ref.isects.length;
				
				
				//1つもwatch中のrectとintersectしてない → 新規追加
				if(isectsNum == 0){
					if(rec_ref.width*rec_ref.height <= this.__min_size_new_rect_rate){
						//trace("tooSmall"+(rec_ref.width*rec_ref.height)+":"+this.__min_size_new_rect_rate);
						recs.splice(Number(i), 1);
						i--;
					}else{
						//trace('CREATE !!!');
						rec_ref.id = ++this.__idCounter;
						rec_ref.event = TrakMultiEvent.CREATE;
						rec_ref.life = this.__rect_life;
						recs_candidates.push(rec_ref);
					}
				
				}else if(isectsNum >= 2){
					var rect_integrate:TrakRectangle = null;
					for(var j:int=0; j<isectsNum-1; j++){
						var rec_now:TrakRectangle = rec_ref.isects[j];
						var rec_next:TrakRectangle = rec_ref.isects[j+1];
						rect_integrate = (rec_now.isects.length >= rec_next.isects.length) ? rec_now : rec_next;
					}
					for(j=0; j<isectsNum; j++){
						if(rect_integrate != rec_ref.isects[j]){
							//trace('INTEGRATE');
							var rec_disappear:TrakRectangle = rec_ref.isects[j];
							//Should be integrate
							rec_disappear.event = TrakMultiEvent.DISAPPEAR;
							rec_disappear.integarateTarget =  rect_integrate;
							//rec_disappear.integarateTarget.life = 1;
							recs_disappeared.push(rec_disappear);
							
							for(var k:int=0; k<this.__recs_watching.length; k++){
								if(this.__recs_watching[k] == rec_disappear){
									this.__recs_watching.splice(k, 1);
									break;
								}
							}
						}
					}
				}
			}
			
			for(i=0; i<this.__recs_watching.length; i++){
				rec_watch = this.__recs_watching[i];
				isectsNum = rec_watch.isects.length;
				if(isectsNum == 0){
//					trace("intersect: " + rec_watch.id + " life:"+ rec_watch.life + " event: " + rec_watch.event);
					rec_watch.life -- ;
					//trace('stay');
					rec_watch.event = TrakMultiEvent.STAY;
					recs_candidates.push(rec_watch);
				}else if(isectsNum == 1){
//					trace("intersect: " + rec_watch.id  + " life:" + rec_watch.life);
					rec_watch.isects[0].id = rec_watch.id;
					rec_watch.isects[0].event = TrakMultiEvent.MOVE;
					rec_watch.isects[0].life = this.__rect_life;
					recs_candidates.push(rec_watch.isects[0]);
					
				}else{
//					trace("intersect: id:" + rec_watch.id + " length:" + rec_watch.isects.length + " life:" + rec_watch.life);
					while(rec_watch.isects.length > 1){
						union = rec_watch.isects[0].union_rect(rec_watch.isects[1]);
						if(this.__controlMaxUnionSize) {
							if(union.width*union.height < this.__max_size_new_rect_rate) {
								rec_watch.isects[0] = union;
							}
						} else {
							rec_watch.isects[0] = union;
						}
						rec_watch.isects.splice(1, 1);
					}
					rec_watch.isects[0].id = rec_watch.id;
					rec_watch.isects[0].event = TrakMultiEvent.MOVE;
					rec_watch.isects[0].life = this.__rect_life;
					recs_candidates.push(rec_watch.isects[0]);
				}
			}
			this.__recs_watching = [];
			for(i=0; i<recs_candidates.length; i++){
				var rec_item:TrakRectangle = recs_candidates[i];
				
				var infratex:int = (rec_item.width - _rect_inflation <= _rect_inflation) ? 0 : _rect_inflation;
				var infratey:int = (rec_item.height - _rect_inflation <= _rect_inflation) ? 0 : _rect_inflation;
				rec_item.inflate(-infratex, -infratey);
				
				//誰ともintersectしてない寿命切れのrectを削除
				if(rec_item.event == TrakMultiEvent.STAY) {
					if(rec_item.life <= 0){
//						trace("disappear : " + rec_item.id);
						rec_item.event = TrakMultiEvent.DISAPPEAR;
						recs_disappeared.push(rec_item);
					}else{
						this.__recs_watching.push(rec_item);
					}
				//移動、統合物
				}else if(rec_item.event == TrakMultiEvent.MOVE) {
					this.__recs_watching.push(rec_item);
				
				//新規
				}else if(rec_item.event == TrakMultiEvent.CREATE) {
					this.__recs_watching.push(rec_item);
				}
			}
	
			// dispatch events
			for(i=0; i<this.__recs_watching.length; i++){
				tgt_rect = this.__recs_watching[i];
//				trace(tgt_rect.event + " :: " + tgt_rect.id + " :: "+tgt_rect);
				this.dispatchEvent(new TrakMultiEvent(tgt_rect.event, tgt_rect.id, Rectangle(tgt_rect)));
				this.__boxes.push(tgt_rect);
			}
			
			for(i=0; i<recs_disappeared.length; i++){
				tgt_rect = recs_disappeared[i];
				this.dispatchEvent(new TrakMultiEvent(tgt_rect.event, tgt_rect.id, Rectangle(tgt_rect)));
			}
			
			this.__act = (this.__recs_watching.length>0);
			//dispatchEvent({type:FuelTrackingRectangle.UPDATE});
		}

		/**
		 * The image the MotionTracker is working from
		 */
		public function get trackingImage():BitmapData { 
			return this.__nowFrame; 
		}

		/**
		 * The area of the image the MotionTracker is working from
		 */
		public function get trackingArea():Rectangle { 
			return new Rectangle(this.__input.x, this.__input.y, this.__input.width, this.__input.height); 
		}

		/**
		 * Whether or not movement is currently being detected
		 */
		public function get hasMovement():Boolean { 
			return this.__act; 
		}

		/**
		 * The area in which movement is being detected
		 * Will return a Rectangle box, or an array of rectangles if there are multiples detections
		 */
		public function get motionArea():* { 
			if(this.__boxes) return this.__boxes;
			else return this.__box; 
		}

		/* INPUT */
		
		/**
		 * The video (usualy created from a Camera) used to track motion
		 */
		public function get input():Video { 
			return this.__input; 
		}

		public function set input(v:Video):void {
			this.__input = v;
			if ( this.__nowFrame != null ) { 
				this.__nowFrame.dispose(); 
				this.__oldFrame.dispose(); 				this.__difFrame.dispose(); 
			}
			this.__nowFrame = new BitmapData(v.width, v.height, false, 0);
			this.__oldFrame = new BitmapData(v.width, v.height, false, 0);
			this.__difFrame = new BitmapData(v.width, v.height, false, 0);
		
		}

		/* BLUR */
		
		/**
		 * the blur being applied to the input in order to improve accuracy
		 */
		public function get blur():int { 
			return this.__blurFilter.blurX; 
		}

		public function set blur( n:int ):void { 
			this.__blurFilter.blurX = this.__blurFilter.blurY = n; 
		}
		
		/* MULTI TRACKING */
		
		public function get multiTracking():Boolean { 
			return this.__multiTracking; 
		}

		public function set multiTracking( _val:Boolean ):void { 
			this.__multiTracking = _val;
		}
		

		/* BRIGHTNESS */
		
		/**
		 * The brightness filter being applied to the input
		 */
		public function get brightness():int { 
			return this.__colourMatrix.brightness; 
		}

		public function set brightness( n:int ):void {
			this.__colourMatrix.brightness = n;
			this.__colourMatrixFilter = new ColorMatrixFilter(this.__colourMatrix.getMatrix());
		}

		/* CONTRAST */
		
		/**
		 * The contrast filter being applied to the input
		 */
		public function get contrast():int { 
			return this.__colourMatrix.contrast; 
		}

		public function set contrast( n:int ):void {
			this.__colourMatrix.contrast = n;
			this.__colourMatrixFilter = new ColorMatrixFilter(this.__colourMatrix.getMatrix());
		}
		
		public function get saturation():int { 
			return this.__colourMatrix.contrast; 
		}

		public function set saturation( n:int ):void {
			this.__colourMatrix.saturation = n;
			this.__colourMatrixFilter = new ColorMatrixFilter(this.__colourMatrix.getMatrix());
		}

		/* MIN AREA */
		
		
		/**
		 * The minimum area (percent of the input dimensions) of movement to be considered movement
		 */
		public function get minBlob():int { 
			return this.__min; 
		}

		public function set minBlob( n:int ):void {
			if ( n < 0 ) return;
			this.__min = n;
		}

		public function get maxBlob():int { 
			return this.__max; 
		}

		public function set maxBlob( n:int ):void {
			if ( n < 0 ) return;
			this.__max = n;
		}

		/* FLIP INPUT */
		
		/**
		 * Whether or not to flip the input for mirroring
		 */
		public function get flipInput():Boolean { 
			return this.__matrix.a < 1; 
		}

		public function set flipInput( b:Boolean ):void {
			this.__matrix = new Matrix();
			if (b) { 
				this.__matrix.translate(-this.__input.width, 0); 
				this.__matrix.scale(-1, 1); 
			}
		}

		public function destroy():void {
			if(this.__oldFrame) {
				this.__oldFrame.dispose();
				this.__oldFrame = null;
			}
			if(this.__nowFrame) {
				this.__nowFrame.dispose();
				this.__nowFrame = null;
			}
			this.__input = null;
			this.__blurFilter = null;			this.__colourMatrix = null;			this.__colourMatrixFilter = null;			this.__box = null;			this.__act = false;			this.__matrix = null;			this.__boxes = null;
		}
		
		public function get controlMaxUnionSize():Boolean {
			return __controlMaxUnionSize;
		}
		
		public function set controlMaxUnionSize(_controlMaxUnionSize:Boolean):void {
			__controlMaxUnionSize = _controlMaxUnionSize;
		}
	}
}
